#!/usr/bin/env python
# encoding: utf-8
"""
checkPackageSignatures.py

"""
# Copyright 2012 Greg Neagle.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import sys
import os
import optparse
import plistlib
import shutil
import subprocess
import tempfile
from xml.parsers.expat import ExpatError

# dmg helpers

def getFirstPlist(textString):
    """Gets the next plist from a text string that may contain one or
    more text-style plists.
    Returns a tuple - the first plist (if any) and the remaining
    string after the plist"""
    plist_header = '<?xml version'
    plist_footer = '</plist>'
    plist_start_index = textString.find(plist_header)
    if plist_start_index == -1:
        # not found
        return ("", textString)
    plist_end_index = textString.find(
        plist_footer, plist_start_index + len(plist_header))
    if plist_end_index == -1:
        # not found
        return ("", textString)
    # adjust end value
    plist_end_index = plist_end_index + len(plist_footer)
    return (textString[plist_start_index:plist_end_index],
            textString[plist_end_index:])


def DMGhasSLA(dmgpath):
    '''Returns true if dmg has a Software License Agreement.
    These dmgs normally cannot be attached without user intervention'''
    hasSLA = False
    proc = subprocess.Popen(
                ['/usr/bin/hdiutil', 'imageinfo', dmgpath, '-plist'],
                 bufsize=-1, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    (out, err) = proc.communicate()
    if err:
        print >> sys.stderr, (
            'hdiutil error %s with image %s.' % (err, dmgpath))
    (pliststr, out) = getFirstPlist(out)
    if pliststr:
        try:
            plist = plistlib.readPlistFromString(pliststr)
            properties = plist.get('Properties')
            if properties:
                hasSLA = properties.get('Software License Agreement', False)
        except ExpatError:
            pass

    return hasSLA


def mountdmg(dmgpath):
    """
    Attempts to mount the dmg at dmgpath
    and returns a list of mountpoints
    Skip verification for speed.
    """
    mountpoints = []
    dmgname = os.path.basename(dmgpath)
    stdin = ''
    if DMGhasSLA(dmgpath):
        stdin = 'Y\n'
        
    cmd = ['/usr/bin/hdiutil', 'attach', dmgpath,
                '-mountRandom', '/tmp', '-nobrowse', '-plist',
                '-noverify', '-owners', 'on']
    proc = subprocess.Popen(cmd, bufsize=-1, 
        stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
    (out, err) = proc.communicate(stdin)
    if proc.returncode:
        print >> sys.stderr, 'Error: "%s" while mounting %s.' % (err, dmgname)
    (pliststr, out) = getFirstPlist(out)
    if pliststr:
        plist = plistlib.readPlistFromString(pliststr)
        for entity in plist['system-entities']:
            if 'mount-point' in entity:
                mountpoints.append(entity['mount-point'])

    return mountpoints


def unmountdmg(mountpoint):
    """
    Unmounts the dmg at mountpoint
    """
    proc = subprocess.Popen(['/usr/bin/hdiutil', 'detach', mountpoint],
                                bufsize=-1, stdout=subprocess.PIPE,
                                stderr=subprocess.PIPE)
    (unused_output, err) = proc.communicate()
    if proc.returncode:
        print >> sys.stderr, 'Polite unmount failed: %s' % err
        print >> sys.stderr, 'Attempting to force unmount %s' % mountpoint
        # try forcing the unmount
        retcode = subprocess.call(['/usr/bin/hdiutil', 'detach', mountpoint,
                                   '-force'])
        if retcode:
            print >> sys.stderr, 'Failed to unmount %s' % mountpoint
            

def str_to_ascii(s):
    """Given str (unicode, latin-1, or not) return ascii.

    Args:
      s: str, likely in Unicode-16BE, UTF-8, or Latin-1 charset
    Returns:
      str, ascii form, no >7bit chars
    """
    try:
        return unicode(s).encode('ascii', 'ignore')
    except UnicodeDecodeError:
        return s.decode('ascii', 'ignore')
            
            
def checkSignature(pkg):
    proc = subprocess.Popen(
        ['/usr/sbin/pkgutil', '--check-signature', pkg],
        stderr=subprocess.PIPE, stdout=subprocess.PIPE)
    stdout, stderr = proc.communicate()
    if proc.returncode:
        if not "Status: no signature" in stdout:
            lines = stdout.splitlines()
            try:
                if "Package" in lines[0] and "Status:" in lines[1]:
                    return "\n".join(lines[0:2])
            except IndexError:
                pass
            return stdout + stderr
    return ''


def checkDiskImage(dmgpath):
    mountpoints = mountdmg(dmgpath)
    if not mountpoints:
        return
    # search mounted diskimage for all flat packages.
    mountpoint = mountpoints[0]
    printed_dmg_path = False
    for dirpath, dirnames, filenames in os.walk(mountpoint):
        for name in filenames:
            if name.endswith('.pkg'):
                filepath = os.path.join(dirpath, name)
                status = checkSignature(filepath)
                if status:
                    if not printed_dmg_path:
                        printed_dmg_path = True
                        print "%s:" % dmgpath
                    print status
                
    unmountdmg(mountpoint)

def main():
    usage = ('%prog /path/to/check')
    p = optparse.OptionParser(usage=usage)
    options, arguments = p.parse_args()
    
    if len(arguments) == 0 or len(arguments) > 1:
        print >> sys.stderr, "Wrong number of parameters!"
        p.print_usage()
        exit()
        
    path_to_check = arguments[0]
    if not os.path.exists(path_to_check):
        print >> sys.stderr, "%s doesn't exist!" % path_to_check
    
    if os.path.isfile(path_to_check) and path_to_check.endswith(".dmg"):
        checkDiskImage(path_to_check)
        
    if os.path.isdir(path_to_check):
        for dirpath, dirnames, filenames in os.walk(path_to_check):
            for name in filenames:
                filepath = os.path.join(dirpath, name)
								
                if not name.startswith('._') and name.endswith('.pkg'):
                    status = checkSignature(filepath)
                    if status:
                        print "%s:" % filepath
                        print status
                if not name.startswith('._') and name.endswith('.dmg'):
                    checkDiskImage(filepath)


if __name__ == '__main__':
    main()

